import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.HashMap;

/**
* class ServerIOThread: IO thread for a client
*  - receive and analyze coming messages from the attached client
*  - update maze and send back the updating messages to client
*  - ServerState : 
*  		()means send out  []means receive   {}means crash event
*  		PS_NO_BS --(backup)--> PS_WAIT_BS --[backup-ready]--> PS_WITH_BS   BS
*          ^                       |                             |		   |
*          |                       |                             |		   |
*          |-----[backup-fail]-----|                             |		   |
*          |----------------------{BS crashes}-------------------|         |
*          |----------------------{PS crashes}-----------------------------|             
*/
public class ServerIOThread extends Thread {
	private Socket socket = null;
	private HashMap<Socket, PrintWriter> sockets; // all socket's writer
	private int clientnum; // this client's id(int)
	private Player player; // this client's player
	private Maze maze; // the whole maze
	private PrintWriter cos; // os to current client
	private boolean serverClient; //  this client is a server-client (BS or PS)

	private static int bsPort; // backup server's port 
	private static PrintWriter bsos = null; // os to the backup server
	private static String bsip = null; // backup server's ip
	
	private static ServerState state = ServerState.PS_NO_BS; // the server's state
	
	public ServerIOThread(Socket socket, HashMap<Socket, PrintWriter> sockets, int clientnum, Maze maze, boolean isPS) {
		this.socket = socket;
		this.sockets = sockets;
		this.cos = sockets.get(socket);
		this.clientnum = clientnum;
		this.maze = maze;
		this.serverClient = false;
		if (!isPS) {
			state = ServerState.BS;
		}
	}
	
	public void run() {
		boolean running = true;
		BufferedReader is = null;
		try {
			is = new BufferedReader(new InputStreamReader(socket.getInputStream()));
			String request = "";
			
			// during 20sec, clients join game
			if (state != ServerState.BS) {
				// if this server is a primary server
				// Initialise the client
				cos.println("set-id " + clientnum + " " + maze.getN() + " " + maze.getM()); // set client ID for each client
				cos.flush();
				
				// the first client is a ps-client
				if (clientnum == 0) {
					serverClient = true;
				}
				
				// when the 2nd client is a bs-client, tell the client after 2nd client
				if ( clientnum > 1) {
					cos.println("set-bs " + bsip + " " + bsPort);
					cos.flush();
				}
				
				request = is.readLine();  
				
				// wait for 20s
				while (TimeThread.getTime() < 2000) {
					if (!request.equals("")) {
						// nothing to do except born or sync or backup infomation
						if (request.charAt(0) == 'b' || request.charAt(0) == 's') {
							analyze(request);
						} 
					}
					if (is.ready()) {
						request = is.readLine();
					}
					else {
						request = "";
					}
				}
				
				// game begins
				cos.println("game-start");
				cos.flush();
			} // PS end
			
			
			// game start
			// both PS and BS
			while (running) {
				request = is.readLine();
				while (request != null) {
					// analyze and make response to the request
					analyze(request);
					request = is.readLine();
				}
			}
		} catch (IOException e) {
			//e.printStackTrace();
		} finally {
			try {
				is.close();
				cos.close();
				socket.close();
				sockets.remove(socket);
				if (serverClient && (state == ServerState.PS_WITH_BS)) { // serverClient means bs-client
					// What PS do when BS down
					state = ServerState.PS_NO_BS;
					// broadcast to search a new backup server
					for (PrintWriter os : sockets.values()) {
						os.println("backup-search");
						os.flush();
					}
				}
				if (serverClient && (state == ServerState.BS)) { // serverClient means ps-client
					// What BS do when PS down
					state = ServerState.PS_NO_BS;
				}
			} catch (IOException e) {
				//e.printStackTrace();
			}
		}
	}

	private synchronized void analyze(String request) {
		String str[] = request.split("\\s+");
		System.out.println("Server < " + request);
		
		// born NAME IP
		if (str[0].equals("born")) {
			int x = (int)(Math.random() * maze.getN());
			int y = (int)(Math.random() * maze.getN());
			String name = str[1];
			int treasure = 0;
			
			// early clients
			for (String id : maze.getPlayerIdSet()) {
				cos.println("create " + id + " " + maze.getPlayer(id).getName() + " " + maze.getPlayer(id).getTreasure() + " " + maze.getPlayer(id).getX() + " " + maze.getPlayer(id).getY());
				cos.flush();
			}

			player = new Player(name, treasure, x, y);
			maze.addPlayer(String.valueOf(clientnum), player);
			
			// create ID NAME TREASURE X Y
			if (bsos != null) {
				bsos.println("backup-create " + clientnum + " " + name + " " + treasure + " " + x + " " + y);
				bsos.flush();
			}
			for (PrintWriter os : sockets.values()) {
				os.println("create " + clientnum + " " + name + " " + treasure + " " + x + " " + y);
				os.flush();
			}
			
			cos.println("treasure-info"+ maze.getTreasureList());
			cos.flush();	
		}
		
		/*updatye player psoition and treasure; clear the treasure at the cell of maze*/
		// move [direction]
		else if (str[0].equals("move")) {
			if (str[1].equals("nomove")) {
				cos.println("set-player " + clientnum + " " + maze.getPlayer(String.valueOf(clientnum)).getX() + " " + maze.getPlayer(String.valueOf(clientnum)).getY() + " " + 0 + " " + maze.getPlayer(String.valueOf(clientnum)).getTreasure());
				cos.flush();
			} 
			
			else {
				
				if (str[1].equals("east")) {
					maze.getPlayer(String.valueOf(clientnum)).goEast(maze.getN());
				}
				
				else if (str[1].equals("west")) {
					maze.getPlayer(String.valueOf(clientnum)).goWest();
				}
				
				else if (str[1].equals("north")) {
					maze.getPlayer(String.valueOf(clientnum)).goNorth();
				}
	
				else if (str[1].equals("south")) {
					maze.getPlayer(String.valueOf(clientnum)).goSouth(maze.getN());
				}
				
				int x = maze.getPlayer(String.valueOf(clientnum)).getX();
				int y = maze.getPlayer(String.valueOf(clientnum)).getY();
				
				int newTreasure = maze.clearTreasure(x, y);
				int totalTreasure = maze.getPlayer(String.valueOf(clientnum)).addTreasure(newTreasure);
				
				/* send updated info  back to BS and all clients */				
				// set-player ID X Y NewTSnum totalSnum
				if (bsos != null) {
					bsos.println("backup-set-player " + clientnum + " " + x + " " + y + " " + newTreasure + " " + totalTreasure);
					bsos.flush();
				}
				for (PrintWriter os : sockets.values()) {
					os.println("set-player " + clientnum + " " + x + " " + y + " " + newTreasure + " " + totalTreasure);
					os.flush();
				}
			}
		}
		
		// treasure-info
		else if (str[0].equals("treasure-info")) {
			// treasure-info [x1 y1 num1 ...]
			cos.println("treasure-info" + maze.getTreasureList());
			cos.flush();
		}
		
		// sync CTIME
		else if (str[0].equals("sync")) {
			// sync CTIME STIME
			cos.println(request + " " + TimeThread.getTime());
			cos.flush();
		}
		
		/*
		 *	the BS has been done:
		 *	- the PS connect to the BS
		 *	- backup the treasure info and the players
		 *	- tell all clients the BS's ip and port
		 */
		else if (str[0].equals("backup-ready")) {
			ServerIOThread.setBSIP(socket.getInetAddress().getHostAddress());
			bsPort = Integer.parseInt(str[1]);
			state = ServerState.PS_WITH_BS;
			serverClient = true;
			
			Socket bsSocket;
			try {
				bsSocket = new Socket(socket.getInetAddress(), bsPort);
				bsos = new PrintWriter(bsSocket.getOutputStream());
			} catch (IOException e) {
				e.printStackTrace();
			}
			
			bsos.println("backup-treasure" + maze.getTreasureList());
			bsos.flush();
			for (String id : maze.getPlayerIdSet()) {
				bsos.println("backup-create " + id + " " + maze.getPlayer(id).getName() + " " + maze.getPlayer(id).getTreasure() + " " + maze.getPlayer(id).getX() + " " + maze.getPlayer(id).getY());
				bsos.flush();
			}
			
			for (PrintWriter os : sockets.values()) {
				os.println("set-bs " + bsip + " " + bsPort);
				os.flush();
			}
		}
		
		// the client refuse to set up a backup server
		else if (str[0].equals("backup-fail")) {
			state = ServerState.PS_NO_BS;
			serverClient = true;
		}
		
		// the client tells the PS that I can setup a BS
		else if (str[0].equals("backup-apply")) {
			// just a signal, does nothing
		}
		
		
		// below is the message received by the BackupServer
		// backup the treasure info
		else if (str[0].equals("backup-treasure")) {
			int treasureNum = str.length;
			for (int i = 1; i < treasureNum; i = i + 3) {
				maze.setTreasure(Integer.parseInt(str[i]), Integer.parseInt(str[i+1]), Integer.parseInt(str[i+2]));
			}
			serverClient = true;
		}
		
		// backup the player born
		else if (str[0].equals("backup-create")) {
			Player p = new Player(str[2], Integer.parseInt(str[3]), Integer.parseInt(str[4]), Integer.parseInt(str[5]));
			maze.addPlayer(str[1], p);
		}
		
		// backup the move
		else if (str[0].equals("backup-set-player")) {
			String id = str[1];
			maze.getPlayer(id).setX(Integer.parseInt(str[2]));
			maze.getPlayer(id).setY(Integer.parseInt(str[3]));
			maze.getPlayer(id).addTreasure(Integer.parseInt(str[4]));
		}
		
		// the client tells the BS its clientID
		else if (str[0].equals("set-id")) {
			clientnum = Integer.parseInt(str[1]);
			player = maze.getPlayer(str[1]);
		}
		
		/*
		 * How to set-up backup
		 * 	- if this is a PS without BS and not waiting for a BS (PS_NO_BS), and if this client is not a ps-client
		 * 	- then tell the client to be a bs, and set the state to be PS_WAIT_BS
		 * 	- when there is no reply, set the state PS_WAIT_BS to PS_NO_BS
		 */

		if (state == ServerState.PS_NO_BS && (!serverClient)) {
			cos.println("backup");
			cos.flush();
			state = ServerState.PS_WAIT_BS;
		}
		
	}
	
	public static void setBSOS(PrintWriter bsos) {
		ServerIOThread.bsos = bsos;
	}

	public static void setBSIP(String bsip) {
		ServerIOThread.bsip = bsip;
	}
}
